package gameStates.network;

import interfaces.hud.chat.ChatType;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Callable;

import logic.common.Squad;
import logic.common.SquadListener;
import logic.common.game.Game;
import logic.common.player.Player;
import logic.common.player.playerListeners.HunterChangedEvent;
import logic.common.player.playerListeners.KillAssistEvent;
import logic.common.player.playerListeners.KillEvent;
import logic.common.player.playerListeners.MissionAssistEvent;
import logic.common.player.playerListeners.NearStationChangedEvent;
import logic.common.player.playerListeners.PlayerListener;
import logic.common.player.playerListeners.PlayerStationListener;
import logic.common.player.playerListeners.PlayerValueListener;
import logic.common.player.playerListeners.RespawnEvent;
import logic.common.player.playerListeners.SelectedSpawnChangedEvent;
import logic.common.player.playerListeners.TargetChangedEvent;
import logic.common.team.Fraction;
import logic.common.team.Team;
import logic.common.team.processes.TeamProcess;
import logic.nodes.TeamNode;
import logic.nodes.damagable.DamagableNode;
import logic.nodes.nodeSettings.upgrades.UpgradableSettings;
import logic.nodes.nodeSettings.upgrades.UpgradeEvent;
import logic.nodes.nodeSettings.upgrades.UpgradeListener;
import logic.ships.Ship;
import logic.ships.frigate.Frigate;
import logic.ships.hunter.Hunter;
import logic.ships.mothership.MotherShip;
import logic.status.StatusChangedEvent;
import logic.weapons.Weapon;
import logic.weapons.WeaponFire;
import logic.weapons.WeaponProperties;
import logic.weapons.projectileWeapons.heatSeeker.HeatSeekerWeapon;
import main.InitGame;
import map.spawnStation.SpawnPosition;
import map.spawnStation.SpawnStation;
import networking.SyncManager;
import networking.Synchronizable;
import networking.messages.ChatMessage;
import networking.messages.SquadUpdateMessage;
import networking.messages.playerMessages.PlayerAddMessage;
import networking.messages.playerMessages.PlayerUpdateMessage;
import networking.messages.playerMessages.PlayerUpdateValueType;
import networking.messages.syncMessages.NodeSyncMessage;
import networking.messages.syncMessages.UpgradeMessage;
import networking.messages.syncMessages.ValueType;
import networking.messages.weaponMessages.WeaponAddMessage;
import networking.messages.weaponMessages.WeaponFireAddMessage;

import com.jme.math.Vector3f;
import com.jme.util.GameTaskQueueManager;

import factories.WeaponFactory;
import gameStates.SimpleGameState;
import gameStates.absGamesStates.AbsIngameState;

/**
 * @author Wasserleiche
 */
public abstract class NetworkState extends SimpleGameState {
	
	public static final int SYNC_UPDATE_RATE = 20;
	
	protected IDContainer<DamagableNode> objects;
	
	protected IDContainer<Weapon> weapons;
	
	protected IDContainer<WeaponFire> weaponFires;
	
	protected IDContainer<Player> players;

	protected IDContainer<TeamProcess> teamProcesses;
	
	protected HashMap<Integer, Short> humanPlayers;
	protected HashMap<Short, Integer> humanPlayersShort;
	
	protected HashSet<Integer> updatingPlayers;
	
	protected Collection<Synchronizable> syncHeap;
	
	protected boolean isServer;
	protected AbsIngameState ingameState;
	
	protected Squad updatingSquad;
	
	public NetworkState(String name, boolean isServer) {
		super(name);
		this.isServer = isServer;
		
		initStructures();
	}
	
	protected void initStructures() {
		weapons = new IDContainer<Weapon>();
		objects = new IDContainer<DamagableNode>();
		weaponFires = new IDContainer<WeaponFire>();
		players = new IDContainer<Player>();
		teamProcesses = new IDContainer<TeamProcess>();
		
		humanPlayers = new HashMap<Integer, Short>();
		humanPlayersShort = new HashMap<Short, Integer>();
		
		updatingPlayers = new HashSet<Integer>();
		
		syncHeap = new ArrayList<Synchronizable>();
	}
	
	@Override
	public void cleanup() {
		super.cleanup();
		ingameState = null;
		initStructures();
	}
	
	@Override
	public void render(float tpf) {}
	
	public abstract SyncManager getSyncManager();
	
	public abstract void registerSyncObject(Synchronizable obj);
	
	protected abstract void sendWeaponAddMessage(Weapon weapon);
	
	protected abstract void sendPlayerUpdateMessage(PlayerUpdateMessage pum);
	
	protected abstract void sendUpgradeMessage(UpgradeMessage um);
	
	protected abstract SquadListener getSquadListener(Squad squad);
	
	protected abstract void handleNearSpawnStationChanged(NearStationChangedEvent event, int playerID);
	
	protected abstract void handleKillEvent(KillEvent event);
	
	public Player getPlayer(int playerID) { return players.get(playerID); }
	
	public void unregisterWeaponFire(WeaponFire fire) { weaponFires.unregister(fire.getID()); }
	
	public Weapon getWeapon(int weaponID) { return weapons.get(weaponID); }
	
	protected void handleNodeSyncMessage(final NodeSyncMessage nsm) {
		if(nsm.getType() == null) return;
		
		final DamagableNode node = objects.get(nsm.getObjectID());
		if(node != null) {
			GameTaskQueueManager.getManager().update(new Callable<Void>() {
				@Override
				public Void call() throws Exception {
					node.applySyncMessage(nsm);
					return null;
				}
			});
		}
	}
	
	public void registerStaticObject(TeamNode node) {
		assert(node instanceof MotherShip || node instanceof SpawnStation);
		objects.register(node, node.getID());
	}
	
	public void registerWeaponFire(WeaponFire fire) {
		weaponFires.register(fire);
	}
	
	public void unregisterObject(Synchronizable obj) {
		if(obj instanceof DamagableNode) {
			DamagableNode node = (DamagableNode)obj;
			objects.unregister(node.getID());
			if(node instanceof Ship) unregisteWeaponsofShip((Ship)node);
		}
	}
	
	protected void unregisteWeaponsofShip(Ship ship) {
		List<Integer> weaponIDs = getWeaponsOfShip(ship.getID());
		for(Integer wid : weaponIDs) {
			Weapon w = weapons.get(wid.intValue());
			if(w != null) {
				final Weapon weapon = w;
				GameTaskQueueManager.getManager().update(new Callable<Void>() {
					@Override
					public Void call() throws Exception {
						unregisterWeapon(weapon);
						return null;
					}
				});
			}
		}
	}
	
	public void registerNewWeapon(Weapon weapon) {
		sendWeaponAddMessage(weapon);
	}
	
	public void unregisterWeapon(Weapon weapon) {
		weapons.unregister(weapon.getID());
	}
	
	protected void removeWeaponFromShip(int weaponIndex) {
		Weapon weapon = weapons.get(weaponIndex);
		unregisterWeapon(weapon);
		
		final Weapon w = weapon;
		final Ship s = getShipOf(weapon);
		GameTaskQueueManager.getManager().update(new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				s.removeWeapon(w);
				return null;
			}
		});
	}
	
	protected void createNewWeapon(final WeaponAddMessage weaponAddMessage) {	
		GameTaskQueueManager.getManager().update(new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				Fraction fraction = weaponAddMessage.getFraction();
				String weaponName = weaponAddMessage.getWeaponName();
				int weaponIndex = weaponAddMessage.getWeaponID();
				Ship ship = (Ship)objects.get(weaponAddMessage.getObjectID());
				if(ship == null) return null;
				
				Weapon newWeapon = null;
				switch(weaponAddMessage.getType()) {
					case HunterWeapon: {
						newWeapon = WeaponFactory.createHunterWeapon(fraction, weaponName, (Hunter)ship);
						break;
					}
					case FrigateWeapon: {
						Team team = ingameState.getGame().getTeam(fraction);
						Frigate frigate = (Frigate)ship;
						WeaponProperties props = team.getFrigateWeaponProperties(weaponAddMessage.getWeaponType());
						
						newWeapon = WeaponFactory.createNewWeapon(weaponName, frigate, props);
						break;
					}
					case MotherShipWeapon: {
						Team team = ingameState.getGame().getTeam(fraction);
						MotherShip ms = team.getMotherShip();
						WeaponProperties props = team.getMSWeaponProperties(weaponAddMessage.getWeaponType());
						
						newWeapon = WeaponFactory.createNewWeapon(weaponName, ms, props);
						break;
					}
				}
				
				if(newWeapon != null) {
					weapons.register(newWeapon, weaponIndex);
					
					final Weapon w = newWeapon;
					final Ship s = ship;
					
					s.addWeapon(w);
				}
				
				return null;
			}
		});
	}
	
	public UpgradeListener getNewUpgradeListener(final DamagableNode node) {
		return new UpgradeListener() {
			@Override
			public void upgraded(UpgradeEvent event) {
				UpgradeMessage um = new UpgradeMessage(node, event.getKey(), event.getValue());
				sendUpgradeMessage(um);
			}
		};
	}
	
	protected void handleUpgradeMessage(UpgradeMessage um) {
		UpgradableSettings settings = null;
		switch(um.getType()) {
			case Ship: 	DamagableNode node = objects.get(um.getObjectID()); 
						node.getSettings();
						break;
			case Weapon: 	Weapon weapon = weapons.get(um.getObjectID());
							settings = weapon.getWeaponProperties();
							break;
		}
		
		if(settings != null) settings.upgrade(um.getKey(), true);
	}
	
	public Ship getShipOf(Weapon weapon) { return (Ship)objects.get(weapon.getShip().getID()); }
		
	public List<Integer> getWeaponsOfShip(int objectID) {
		List<Integer> weapons = new ArrayList<Integer>();
		
		Ship ship = (Ship)objects.get(objectID);
		if(ship == null) return weapons;
		
		for(List<Weapon> weaponList : ship.getAllWeapons().values()) {
			addWeaponIndeces(weaponList, weapons);
		}
		
		return weapons;
	}
	
	protected void addWeaponIndeces(Collection<? extends Weapon> weapons, List<Integer> weaponIndeces) {
		for(Weapon w : weapons) {
			weaponIndeces.add(Integer.valueOf(w.getID()));
		}
	}
	
	protected void fireWeapon(final WeaponFireAddMessage wfam, final boolean dummy) {
		final Weapon weapon = weapons.get(wfam.getWeaponID());
		
		if(weapon != null && !(dummy && weapon instanceof HeatSeekerWeapon)) {
			GameTaskQueueManager.getManager().update(new Callable<Void>() {
				@Override
				public Void call() throws Exception {
					if(wfam.getWeaponFireClientID() != 0) 
						weapon.setupClient(wfam.getWeaponFireClientID(), wfam.getPlayerId());
					
					WeaponFire fire = weapon.fire(wfam.getDirection(), dummy);
					weapon.resetClient();
					if(fire != null) {
						long thisTime = System.currentTimeMillis();
						long diff = thisTime - wfam.getLocalTime();
						float t = diff / 1000f;
						compensateWeaponsFire(fire, t);
						ingameState.addNode(fire);
						
						if(!InitGame.get().isServer()) registerWeaponFire(fire, wfam.getWeaponFireID());
					}
					return null;
				}
			});
		}
	}
	
	protected void compensateWeaponsFire(WeaponFire fire, float t) {
		float fireSpeed = fire.getWeapon().getWeaponProperties().getSpeed();
		Vector3f diffVector = fire.getDirection().normalize().multLocal(t * fireSpeed);
		fire.getLocalTranslation().addLocal(diffVector);
	}
	
	public void registerWeaponFire(WeaponFire fire, int fireID) {
		weaponFires.register(fire, fireID);
	}
	
	public void registerNewPlayer(PlayerAddMessage pam) {
		Game game = ingameState.getGame();
		Team team = game.getTeam(pam.getFraction());
		Team enemyTeam = game.getTeam(pam.getEnemyFraction());
		
		Player newPlayer = new Player(pam, team, enemyTeam);
		players.register(newPlayer, pam.getPlayerID());
	}
	
	public boolean isServer() { return isServer; }
	
	public AbsIngameState getIngameState() { return ingameState; }
	
	public void setIngameState(AbsIngameState ingameState) {
		this.ingameState = ingameState;
		
		addSquadListener(ingameState.getGame().getTeamA());
		addSquadListener(ingameState.getGame().getTeamB());
	}
	
	protected void addSquadListener(Team team) {
		Iterator<Squad> iter = team.getAllSquadIterator();
		while(iter.hasNext()) {
			Squad squad = iter.next();
			addListener(squad, getSquadListener(squad));
		}
	}
	
	protected void addUpdatingPlayer(int playerID) {
		updatingPlayers.add(Integer.valueOf(playerID));
	}
	
	protected void removeUpdatingPlayer(int playerID) {
		updatingPlayers.remove(Integer.valueOf(playerID));
	}
	
	protected boolean isUpdatingPlayer(int playerID) {
		return ingameState == null || updatingPlayers.contains(new Integer(playerID));
	}
	
	protected void applyPlayerUpdateMessage(final PlayerUpdateMessage pum) {
		final Player player = players.get(pum.getPlayerID());
		if(player != null) {
			GameTaskQueueManager.getManager().update(new Callable<Void>() {
				@Override
				public Void call() throws Exception {
					int playerID = player.getID();
					addUpdatingPlayer(playerID);
					
					if(!player.applyUpdateMessage(pum)) applyPlayerUpdateMessageID(pum, player);
					
					removeUpdatingPlayer(playerID);
					return null;
				}
			});
		}
	}
	
	protected void applyPlayerUpdateMessageID(PlayerUpdateMessage pum, Player player) {
		DamagableNode object = objects.get(pum.getValue());
		if(object != null) {
			switch(pum.getType()) {
				case TargetID: {
					player.setCurrentTarget((Hunter)object);
					break;
				}
				case SpawnID: {
					SpawnPosition station = (SpawnPosition)object;
					player.setSpawnLocation(station);
					break;
				}
			}
		} else if(pum.getType() == PlayerUpdateValueType.SpawnID && pum.getValue() == -1) {
			player.setSpawnLocation(player.getTeam().getSpawnPositions().get(0));
		}
	}
	
	public PlayerListener createPlayerListener() {
		return new PlayerListener() {
			@Override
			public void targetChanged(TargetChangedEvent event) {
				manageTargetChangedEvent(event);
			}
			
			@Override
			public void respawned(RespawnEvent event) {
				int playerIndex = event.getRespawnedPlayer().getID();
				if(isUpdatingPlayer(playerIndex)) return;
				
				PlayerUpdateValueType type = PlayerUpdateValueType.Respawn;
				Player player = players.get(playerIndex);
				if(player != null) {
					Hunter hunter = player.getHunter();
					if(hunter != null) {
						int hunterID = hunter.getID();
						PlayerUpdateMessage pum = new PlayerUpdateMessage(playerIndex, hunterID, type);
						sendPlayerUpdateMessage(pum);
					}
				}
			}
			
			@Override
			public void killed(KillEvent event) {
				handleKillEvent(event);
			}
			
			@Override
			public void hunterChanged(HunterChangedEvent event) {}
			@Override
			public void killAssist(KillAssistEvent event) {}
			@Override
			public void missionAssist(MissionAssistEvent event) {}
		};
	}
	
	protected void manageTargetChangedEvent(TargetChangedEvent event) {
		int playerIndex = event.getPlayer().getID();
		if(!event.getPlayer().isHuman() || isUpdatingPlayer(playerIndex)) return;
		
		Hunter newTarget = event.getNewTarget();
		PlayerUpdateValueType type = PlayerUpdateValueType.TargetID;
		PlayerUpdateMessage pum = new PlayerUpdateMessage(playerIndex, newTarget.getID(), type);
		sendPlayerUpdateMessage(pum);
	}
	
	protected Vector3f getPlayerHunterLoc(int playerID) {
		Player player = players.get(playerID);
		if(player == null) return null;
		Hunter hunter = player.getHunter();
		if(hunter == null) return null;
		return hunter.getLocalTranslation();
	}
	
	protected PlayerValueListener getNewPlayerValueListener(final int playerIndex) {
		return new PlayerValueListener() {
			@Override
			public void valueChanged(StatusChangedEvent event) {
				if(isUpdatingPlayer(playerIndex)) return;
				
				ValueType type = event.getType();
				int val = event.getIntValue();
				Vector3f loc = null;
				PlayerUpdateValueType t = PlayerUpdateValueType.valueOf(type.toString());
				if(t == PlayerUpdateValueType.Money) loc = getPlayerHunterLoc(playerIndex);
				
				PlayerUpdateMessage pum = null;
				if(loc == null) pum = new PlayerUpdateMessage(playerIndex, val, t);
				else pum = new PlayerUpdateMessage(playerIndex, val, t, loc);
				
				sendPlayerUpdateMessage(pum);
			}
		};
	}
	
	public Player getPlayerFromShort(short playerId) {
		Short playerShortID = Short.valueOf(playerId);
		if(!humanPlayersShort.containsKey(playerShortID)) return null;
		int intID = humanPlayersShort.get(playerShortID).intValue();
		return players.get(intID);
	}
	
	public short getPlayerShortID(int playerID) {
		return humanPlayers.get(Integer.valueOf(playerID)).shortValue();
	}
	
	protected void applySquadUpdateMessage(final SquadUpdateMessage sum) {
		final Team team = ingameState.getGame().getTeam(sum.getFraction());
		
		final Collection<Player> newMembers = new ArrayList<Player>();
		for(Integer playerID : sum.getPlayerIDs()) {
			Player member = players.get(playerID.intValue());
			if(member != null) newMembers.add(member);
		}
		
		GameTaskQueueManager.getManager().update(new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				updateSquad(team, sum, newMembers);
				return null;
			}
		});
	}
	
	protected void updateSquad(Team team, SquadUpdateMessage sum, Collection<Player> newMembers) {
		updatingSquad = team.getSquad(sum.getSquadID());
		updatingSquad.applyNewMembers(newMembers);
		updatingSquad = null;
	}
	
	public HashMap<Short, Integer> getHumanPlayersShort() { return humanPlayersShort; }
	
	protected ChatMessage createChatMessage(String text, ChatType type) {
		Player p = ingameState.getPlayer();
		Fraction fraction = p.getTeam().getFraction();
		switch(type) {
			case Chat: return new ChatMessage(text, type);
			case TeamChat: return new ChatMessage(text, type, fraction);
			case SquadChat: Squad squad = p.getSquad();
							if(squad != null) return new ChatMessage(text, type, fraction, squad.hashCode());
							return new ChatMessage(text, ChatType.TeamChat, fraction);
		}
		return null;
	}
	
	protected void applyChatMessage(ChatMessage cm) {
		boolean showText = false;
		switch(cm.getType()) {
			case Chat: showText = true; break;
			case TeamChat: showText = cm.getFraction() == ingameState.getPlayer().getTeam().getFraction(); break;
			case SquadChat: Squad squad = ingameState.getPlayer().getSquad();
							showText = squad != null && squad.hashCode() == cm.getSquadIndex(); break;
		}
		
		if(showText) 
			ingameState.getHUDState().showChatText(cm.getChatText(), cm.getType());
	}
	
	protected PlayerStationListener createPlayerStationListener(final int playerID) {
		return new PlayerStationListener() {
			@Override
			public void selectedSpawnPositionChanged(SelectedSpawnChangedEvent event) {
				int objID = event.getSpawnPos().getID();
				PlayerUpdateValueType type = PlayerUpdateValueType.SpawnID;
				PlayerUpdateMessage pum = new PlayerUpdateMessage(playerID, objID, type);
				sendPlayerUpdateMessage(pum);
			}
			
			@Override
			public void nearSpawnStationChanged(NearStationChangedEvent event) {
				handleNearSpawnStationChanged(event, playerID);
			}
		};
	}
}